// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package cn.birdbaby.plugins.deletegen;

import com.intellij.CodeStyleBundle;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.actions.*;
import com.intellij.find.impl.FindInProjectUtil;
import com.intellij.formatting.FormattingModelBuilder;
import com.intellij.ide.lightEdit.LightEditCompatible;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.LanguageFormatting;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import java.util.Arrays;
import java.util.List;
import java.util.regex.PatternSyntaxException;

/**
 * 最小实现/最终实现。实现一个功能，所需要的最基础、最不可少的代码。往往是原理所在
 * 在《最小实现》之外存在的代码，都是一些变式。万变不离其宗——上限
 */
public class BBDeleteGenAnnotationsAction extends AnAction implements DumbAware, LightEditCompatible {
    private static final Logger LOG = Logger.getInstance(BBDeleteGenAnnotationsAction.class);

    private static ReformatFilesOptions myTestOptions;

    public BBDeleteGenAnnotationsAction() {
        setEnabledInModalContext(true);
    }

    @Nullable
    private static DirectoryFormattingOptions getDirectoryFormattingOptions(@NotNull Project project, @NotNull PsiDirectory dir) {
        LayoutDirectoryDialog dialog = new LayoutDirectoryDialog(
                project,
                CodeStyleBundle.message("process.reformat.code"),
                CodeInsightBundle.message("process.scope.directory", dir.getVirtualFile().getPath()),
                VcsFacade.getInstance().hasChanges(dir)
        );

        boolean enableIncludeDirectoriesCb = dir.getSubdirectories().length > 0;
        dialog.setEnabledIncludeSubdirsCb(enableIncludeDirectoriesCb);
        dialog.setSelectedIncludeSubdirsCb(enableIncludeDirectoriesCb);

        if (dialog.showAndGet()) {
            return dialog;
        }
        return null;
    }

    private static void reformatModule(@NotNull Project project,
                                       @Nullable Module moduleContext,
                                       @NotNull ReformatFilesOptions selectedFlags) {
        boolean shouldOptimizeImports = selectedFlags.isOptimizeImports() && !DumbService.getInstance(project).isDumb();
        boolean processOnlyChangedText = selectedFlags.getTextRangeType() == TextRangeType.VCS_CHANGED_TEXT;

        PsiDocumentManager.getInstance(project).commitAllDocuments();

        AbstractLayoutCodeProcessor processor;
        if (moduleContext != null) {
            processor = new ReformatCodeProcessor(project, moduleContext, processOnlyChangedText);
        } else {
            processor = new ReformatCodeProcessor(project, processOnlyChangedText);
        }

        registerScopeFilter(processor, selectedFlags.getSearchScope());
        registerFileMaskFilter(processor, selectedFlags.getFileTypeMask());

        if (shouldOptimizeImports) {
            processor = new OptimizeImportsProcessor(processor);
        }

        if (selectedFlags.isRearrangeCode()) {
            processor = new RearrangeCodeProcessor(processor);
        }

        processor.run();
    }

    public static void registerScopeFilter(@NotNull AbstractLayoutCodeProcessor processor, @Nullable final SearchScope scope) {
        if (scope == null) {
            return;
        }

        processor.addFileFilter(scope::contains);
    }

    public static void registerFileMaskFilter(@NotNull AbstractLayoutCodeProcessor processor, @Nullable String fileTypeMask) {
        if (fileTypeMask == null)
            return;

        final Condition<CharSequence> patternCondition = getFileTypeMaskPattern(fileTypeMask);
        processor.addFileFilter(file -> patternCondition.value(file.getNameSequence()));
    }

    @NotNull
    private static Condition<CharSequence> getFileTypeMaskPattern(@Nullable String mask) {
        try {
            return FindInProjectUtil.createFileMaskCondition(mask);
        } catch (PatternSyntaxException e) {
            LOG.info("Error while processing file mask: ", e);
            return Conditions.alwaysTrue();
        }
    }

    public static PsiFile @NotNull [] convertToPsiFiles(VirtualFile @NotNull [] files, @NotNull Project project) {
        PsiManager psiManager = PsiManager.getInstance(project);
        List<PsiFile> list = PsiUtilCore.toPsiFiles(psiManager, Arrays.asList(files));
        return PsiUtilCore.toPsiFileArray(list);
    }

    private static boolean isActionAvailable(@NotNull AnActionEvent event) {
        DataContext dataContext = event.getDataContext();
        Project project = CommonDataKeys.PROJECT.getData(dataContext);
        if (project == null) {
            return false;
        }

        Editor editor = CommonDataKeys.EDITOR.getData(dataContext);

        final VirtualFile[] files = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext);

        if (editor != null) {
            PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
            if (file == null || file.getVirtualFile() == null) {
                return false;
            }

            if (LanguageFormatting.INSTANCE.forContext(file) != null) {
                return true;
            }
        } else if (files != null && containsOnlyFiles(files)) {
            boolean anyFormatters = false;
            for (VirtualFile virtualFile : files) {
                final PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
                if (psiFile == null) {
                    return false;
                }
                final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(psiFile);
                if (builder != null) {
                    anyFormatters = true;
                    break;
                }
            }
            if (!anyFormatters) {
                return false;
            }
        } else if (files != null && files.length == 1) {
            // skip. Both directories and single files are supported.
        } else if (LangDataKeys.MODULE_CONTEXT.getData(dataContext) == null &&
                PlatformCoreDataKeys.PROJECT_CONTEXT.getData(dataContext) == null) {
            PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(dataContext);
            if (element == null) {
                return false;
            }
            if (!(element instanceof PsiDirectory)) {
                PsiFile file = element.getContainingFile();
                if (file == null || LanguageFormatting.INSTANCE.forContext(file) == null) {
                    return false;
                }
            }
        }
        return true;
    }

    @Nullable
    private static ReformatFilesOptions getReformatFilesOptions(@NotNull Project project, VirtualFile @NotNull [] files) {
        if (ApplicationManager.getApplication().isUnitTestMode()) {
            return myTestOptions;
        }
        ReformatFilesDialog dialog = new ReformatFilesDialog(project, files);
        if (!dialog.showAndGet()) {
            return null;
        }
        return dialog;
    }

    @Nullable
    private static ReformatFilesOptions getLayoutProjectOptions(@NotNull Project project, @Nullable Module module) {
        if (ApplicationManager.getApplication().isUnitTestMode()) {
            return myTestOptions;
        }

        final String text = module != null ? CodeInsightBundle.message("process.scope.module", module.getModuleFilePath())
                : CodeInsightBundle.message("process.scope.project", project.getPresentableUrl());

        final boolean enableOnlyVCSChangedRegions = module != null ? VcsFacade.getInstance().hasChanges(module)
                : VcsFacade.getInstance().hasChanges(project);

        LayoutProjectCodeDialog dialog =
                new LayoutProjectCodeDialog(project, CodeStyleBundle.message("process.reformat.code"), text, enableOnlyVCSChangedRegions);
        if (!dialog.showAndGet()) {
            return null;
        }
        return dialog;
    }

    @TestOnly
    public static void setTestOptions(@NotNull ReformatFilesOptions options) {
        myTestOptions = options;
    }

    static boolean containsOnlyFiles(VirtualFile @NotNull [] files) {
        if (files.length < 1) return false;
        for (VirtualFile virtualFile : files) {
            if (virtualFile.isDirectory()) return false;
        }
        return true;
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent event) {
        DataContext dataContext = event.getDataContext();
        final Project project = CommonDataKeys.PROJECT.getData(dataContext);
        if (project == null) {
            return;
        }
        final Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
        final VirtualFile[] files = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext);

        PsiFile file = null;
        PsiDirectory dir;
        boolean hasSelection = false;

        //打开编辑器了，直接操作当前编辑器的文件
        if (editor != null) {
            file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
            if (file == null) return;
            dir = file.getContainingDirectory();
            hasSelection = editor.getSelectionModel().hasSelection();
        }
        //是不是对模块，或者项目进行操作
        else if (PlatformCoreDataKeys.PROJECT_CONTEXT.getData(dataContext) != null || LangDataKeys.MODULE_CONTEXT.getData(dataContext) != null) {
            Module moduleContext = LangDataKeys.MODULE_CONTEXT.getData(dataContext);
            ReformatFilesOptions selectedFlags = getLayoutProjectOptions(project, moduleContext);
            if (selectedFlags != null) {
                reformatModule(project, moduleContext, selectedFlags);
            }
            return;
        } else {
            PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(dataContext);
            if (element == null) return;
            if (element instanceof PsiDirectoryContainer) {
                dir = ArrayUtil.getFirstElement(((PsiDirectoryContainer) element).getDirectories());
            } else if (element instanceof PsiDirectory) {
                dir = (PsiDirectory) element;
            } else {
                file = element.getContainingFile();
                if (file == null) return;
                dir = file.getContainingDirectory();
            }
        }

        if (file == null && dir != null) {
            DirectoryFormattingOptions options = getDirectoryFormattingOptions(project, dir);
            if (options != null) {
                //清空缓冲区，减少并发修改产生的冲突
                PsiDocumentManager.getInstance(project).commitAllDocuments();
                new BBDeleteGenAnnotationsProcessor(project, editor,options,List.of(convertToPsiFiles(files, project))).processFiles();
            }
            return;
        }

        if (file == null || editor == null) return;

        PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());

        LastRunReformatCodeOptionsProvider provider = new LastRunReformatCodeOptionsProvider(PropertiesComponent.getInstance());
        ReformatCodeRunOptions currentRunOptions = provider.getLastRunOptions(file);

        TextRangeType processingScope = currentRunOptions.getTextRangeType();
        if (hasSelection) {
            processingScope = TextRangeType.SELECTED_TEXT;
        } else if (processingScope == TextRangeType.VCS_CHANGED_TEXT) {
            if (VcsFacade.getInstance().isChangeNotTrackedForFile(project, file)) {
                processingScope = TextRangeType.WHOLE_FILE;
            }
        } else {
            processingScope = TextRangeType.WHOLE_FILE;
        }

        //最后才会跑进这里来
        currentRunOptions.setProcessingScope(processingScope);
        new BBDeleteGenAnnotationsProcessor(project,editor,file).processCode();
    }

    @Override
    public @NotNull ActionUpdateThread getActionUpdateThread() {
        return ActionUpdateThread.BGT;
    }

    @Override
    public void update(@NotNull AnActionEvent event) {
        Presentation presentation = event.getPresentation();
        boolean available = isActionAvailable(event);
        if (event.isFromContextMenu()) {
            presentation.setEnabledAndVisible(available);
        } else {
            presentation.setEnabled(available);
        }
    }
}